package com.fileindexer;

/**
FileIndexer: Places String representations of each filename on the indexed drive into a
				HashMap and stored on disk. That index is used to allow searching for a
				file in a quick fashion. The built in file searching tools on Mac OSX are
				meant for people who are only interested in finding files in their Home
				Directory. FileIndexer goes deeper and allows you to search for, and find
				files that Finder and Spotlight skip.

Author: Brad Howard
Date: 8/1/2012


*/


import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.prefs.Preferences;

import javax.swing.JOptionPane;


public class FileIndexerWorker {

private static final int INITIAL_CAPACITY = 10000;

//key is the filename w/o extension. The list stores all paths with same filename
private static Map<String,List<FileObject>> map;
// GUI uses this array to display matches
private static ArrayList<FileObject> array;

private static ObjectOutputStream outfile;
private static FileOutputStream fileoutput;

//	private final static String user_directory = "/Volumes/FullOfCrap/Users/miniBrad/Desktop";
private final static String user_directory = System.getProperty("user.home");

private static Thread thread;
// stores initial path where indexing starts
private final String startingpath;
// final pointer to send into thread
private static String newfile2;
private volatile static boolean cancel_pushed = false;
private volatile static boolean refresh_excludes = false;

// progress indicator values for indexing
private static int numfiles = 0;
private static int totalfiles = 0;
private static long filesize = 0;
private static int periods = 0;
private static int barvalue = 0;

//============================================================================================================================
//============================================================================================================================

//Constructor
/**
 * @param path is either the starting point to index, or the filename to search for
 * @param find sets whether this is a search or index operation
 */
public FileIndexerWorker(String path, boolean find, boolean refresh)
{
	startingpath = path;
	setRefresh(refresh);

	// if find == false, this is an indexing operation
	if( find == false)
	{
		out("start process");
		startProcess(path);
	}
	else
	{	// find the file that path points to
		doSearch(path);
	}
}
//============================================================================================================================
//============================================================================================================================

/**
 * @returns a HashMap that was generated by previous indexing, or if first time, null
 */
@SuppressWarnings("unchecked")
private  HashMap<String,List<FileObject>> getTheMap()
{
	//this is stored on and retrieved from disk
	HashMap<String,List<FileObject>> input = null;
	ObjectInputStream infile = null;

	//location of stored map
	String selection = FileIndexer.getIndexFilename();
	File file = new File(user_directory + File.separator +selection+"_Index.data");
	//		out(user_directory + File.separator +selection+"_Index.data");

	try {
		infile = new ObjectInputStream(new FileInputStream(file));
	} catch (FileNotFoundException e) {
		//			out("error getting input file. When previous indexing run is canceled,\nyou have prefs, but a missing index file.");
		JOptionPane.showMessageDialog(null, "You must have canceled last indexing\nbecause your file is not complete.\nThe Indexer will run again.");
		//e.printStackTrace();
	} catch (IOException e) {
		out("IOE at infile");
		e.printStackTrace();
	}
	// valid index file found
	if( infile != null)
	{
		FileIndexer.setTheCursor(true);
		try
		{
			input = (HashMap<String,List<FileObject>>) infile.readObject();// this runs for a while, add progress
		}catch( ClassCastException ce)
		{
			out("Casting error");
			ce.printStackTrace();
		}catch(ClassNotFoundException nf)
		{
			out("Class not found error");
			nf.printStackTrace();
		}catch(IOException ioe)
		{
			out("IOE with getting hashmap");
			ioe.printStackTrace();
		}
	}
	//catch all in case JOptionPane is skipped over
	else
		out("infile null");

	//GUI update
	FileIndexer.setTheCursor(false);
	return input;
}

//============================================================================================================================

/**
 * @param pathtoindex is the filename to search for
 * 
 * Called from Constructor is this is a search operation
 */
private void doSearch(final String pathtoindex)
{
	final HashMap<String,List<FileObject>> input = getTheMap();
	
	thread = new Thread(new Runnable()
	{
		@Override
		public void run() {
			array = (ArrayList<FileObject>) input.get(pathtoindex);
		}
	});
//	out("Started search thread");
	thread.start();


	//clears GUI results pane
	FileIndexer.setTextAreaText("");
}



//============================================================================================================================

/**
 * @param pathtoindex is the root folder to start indexing
 * 
 * Walks to directory tree and adds filenames/paths to map then writes map to disk.
 * 
 * The map is a HashMap with the filename sans extension as the key and the absolutePath() as the value
 * 
 */
private void startProcess(final String pathtoindex)
{
	map = new HashMap<String,List<FileObject>>(INITIAL_CAPACITY);

	Preferences prefs = Preferences.userNodeForPackage(getClass());


	//debug stuff
	//		String result = JOptionPane.showInputDialog("Enter any string if you want to reset prefs,\npress Cancel if not.");
	//		if( result != null)
	//		{
	//			try {
	//				prefs.removeNode();
	//				prefs.flush();
	//				prefs = Preferences.userNodeForPackage(getClass());
	//			} catch (BackingStoreException e) {
	//				out("Error removing prefs node");
	//				e.printStackTrace();
	//			}
	//		}
	//end debug stuff

	String prefpath = prefs.get(pathtoindex, "nullpath");
	final String[] excludearray;

	// did not find an exclude list pointer in the prefs node or user chose to refresh index
	if( prefpath.equals("nullpath") || isRefresh() == true)
	{
		out("Prefs not found");
		setRefresh(false);
		excludearray = makeExcludeArray(pathtoindex);
	}
	//found list
	else
	{
		//debug
		out("Pathtoindex: "+pathtoindex);
		String filename = getIndexFilename(pathtoindex);
		out("Filename fed to getExclude: "+filename);
		excludearray = getExcludeArray(filename);

	}

	// GUI info for user about which strings cause paths to be excluded from index
	FileIndexer.getTextArea().append("Paths with the following strings are excluded:\n\n" );
	for(int a = 0; a < excludearray.length; a++)
	{
		FileIndexer.getTextArea().append( excludearray[a]+"\n" );
	}
	FileIndexer.getTextArea().setCaretPosition(0);



	final String newfile;
	File file = null;

	try
	{
		newfile = getIndexFilename(pathtoindex);
		
//		//pointer that is used in thread
		newfile2 = newfile;

		//sets up output stuff for map
		file = new File( newfile );
		file.createNewFile();
		out(file.getAbsolutePath());
		fileoutput = new FileOutputStream(file);
		outfile  = new ObjectOutputStream(fileoutput);

	}catch(IOException ex)
	{
		System.out.println("Error creating out");
		ex.printStackTrace();
	}

	//This is where the fun happens during the indexing. Checks for user cancellation first.

	thread = new Thread(new Runnable()
	{
		@Override
		public void run() {

			boolean canceled = walkToIndex(new File(pathtoindex),excludearray);

			//If they canceled the process, the output stuff is just closed and deleted without
			//writing anything to disk.
			if( canceled == true)
			{
				try {
					outfile.close();
					fileoutput.close();
					File fl = new File( newfile2 );
					if( fl.delete() == false)
						out("Error deleting output file after Indexing canceled");
				} catch (IOException e) {
					out("Indexing canceled. Failed in closing file process");
					e.printStackTrace();
				}
				return;
			}

			//user did not cancel process, so write map to disk
			try {
				outfile.writeObject(map);
			} catch (IOException e1) {
				out("Failed writing hashmap to file");
				e1.printStackTrace();
			}
			try {
				outfile.close();
				fileoutput.close();
			} catch (IOException e1) {
				out("Failed to close file");
				e1.printStackTrace();
			}

			// GUI output into the Results scrollpane
			File file = new File( newfile2 );
			filesize = file.length();
			FileIndexer.setTextAreaText("");
			FileIndexer.hideIndexerInfo();
			FileIndexer.getTextArea().append("Operation Finished.\n");
			if( filesize % 1024000 != filesize)
				FileIndexer.getTextArea().append("Index filesize: "+ (filesize / 1000000) +"mb\n" );
			else
				FileIndexer.getTextArea().append("Index filesize: "+ filesize +"b\n" );

			FileIndexer.getTextArea().append("Total files indexed: " +totalfiles );

			//reset values and display progressbar as full
			filesize = file.length();
			periods = 0;
			FileIndexer.updateBar(100, totalfiles);
			totalfiles = 0;
		}
	});
	thread.start();

}


//============================================================================================================================

/**
 * @param dir is the current file/directory to be processed
 * @param strarray contains the excluded strings to test .contains() with
 * @return the condition of whether or not the user canceled the indexing
 */
private boolean walkToIndex(File dir, String[] strarray)
{
	//user wants to stop indexing. Start return of recursion
	if( cancel_pushed == true)
		return true;

	//used to pass canceled state back up recursion tree
	boolean canceled = false;
	numfiles = 0;

	//test for important things to avoid
	if( dir.toString().startsWith(startingpath) == false || //if we reach this again, there is a problem
			dir.toString().lastIndexOf("/Volumes") > 0 || // if this is true, we have a symbolic link or alias sending us in a loop
			(dir.toString().contains("/Volumes") && startingpath.equals("/") == true) || //another loop condition
			dir.toString().startsWith(".Trash") || //spotlight searches trash
			dir.toString().contains(".DS_Store")) //in every folder, no need to have a node for this
		return false;


	//test this with an empty strarray
	for(String st : strarray)
	{
		if( dir.getAbsolutePath().contains(st) == true)
			return false;
	}



	//Test Block

	//this is a directory. only way this will fail is if a file is initially passed to index
	File listFile[] = dir.listFiles();
	if (listFile != null) {
		for (int i=0; i<listFile.length; i++) {

			if (listFile[i].isDirectory()) {
				canceled = walkToIndex(listFile[i],strarray);
				//user canceled. pass true up recursion tree
				if( canceled == true)
					return true;
			}
			else
			{
				if (listFile[i].isFile())
				{
					//debug
//					String kk = listFile[i].getAbsolutePath();
//					if(kk.contains("Desktop"))
//						out(kk);

					String key2 = null; 
					String filename = listFile[i].getName();

					//skips these
					if( (filename != null) && (filename.contains(".DS_Store") || filename.contains(".localized")) )
						continue;

					//finds extension if there is one
					int period = filename.lastIndexOf('.');

					//gets example:  at.welcome.html
					if( (filename.indexOf('.') != filename.lastIndexOf('.') ) && filename.indexOf('.') > 0 )
					{
						key2 = filename.substring(0, period);
					}
					//gets example:  .hiddenfilename.jpg
					else if( filename.indexOf('.') == 0 && filename.lastIndexOf('.') > 0)
					{
						key2 = filename.substring(1, period);
					}
					//gets example: .hiddenfilename
					else if( filename.indexOf('.') == 0 && filename.lastIndexOf('.') == 0)
					{
						key2 = filename.substring(1, filename.length());
					}
					//gets normal file
					else if(filename.indexOf('.') > 0)
					{
						key2 = filename.substring(0, period);
					}

					//is key2 already in map?
					List<FileObject> checker = map.get(key2);

					//creates FO for this filename
					FileObject fo = new FileObject(listFile[i]);

					//if no checker, this is the first time this key has been used
					// create a new ArrayList to store this, and future collisions
					if( checker == null)
					{
						map.put(key2, checker = new ArrayList<FileObject>() );
					}
					//add this to the bucket
					checker.add(fo);
					numfiles++;

				}
			}
		}
	}

	//	  THIS BLOCK WORKS but it is simple
	//        File listFile[] = dir.listFiles();
	//        if (listFile != null) {
	//            for (int i=0; i<listFile.length; i++) {
	//                if (listFile[i].isDirectory()) {
	//                    canceled = walkToIndex(listFile[i],strarray);
	//                    if( canceled == true)
	//                    	return true;
	//                }
	//                else
	//                {
	//                    if (listFile[i].isFile()) {
	//
	//                    	String key = listFile[i].getName();
	//                    	List<FileObject> checker = map.get(key);
	//                    	FileObject fo = new FileObject(listFile[i]);
	//
	//                    	if( checker == null)
	//                    	{
	//                    		map.put(key, checker = new ArrayList<FileObject>() );
	//                    	}
	//                    	checker.add(fo);
	//                    	numfiles++;
	//
	//                    }
	//                }
	//            }
	//        }

	//updates GUI
	totalfiles += numfiles;
	periods++;
	if( periods % 100 == 0 )
	{
		barvalue += 10;
		FileIndexer.updateBar(barvalue,totalfiles);
	}
	if( periods % 1000 == 0)
	{
		barvalue = 0;
		FileIndexer.updateBar(barvalue,totalfiles);
	}
	return false;
}

//============================================================================================================================

private String[] getExcludeArray(String path)
{
	String[] excludearray = null;
	FileIndexer.setTheCursor(true);
	HashMap<String,List<FileObject>> input = getTheMap();
	FileIndexer.setTheCursor(false);

	if( input == null)
	{
		//JOptionPane.showMessageDialog(null, "Error getting Map.\nSet excludes again and re-index.");
		excludearray = makeExcludeArray(path);
	}
	// found map, do search
	else
	{
		// this list is from the FileObject node that stores the exclude list. This node is the first
		// node created and it stores only this list. The path/filename key is the path to the index
		// file for that drive
		out("");
		
		//debug
//		File ttt = new File(path);
//		out("File: "+ttt.getAbsolutePath());
		
		Set<String> ks = input.keySet();
		int count = 0;
		ArrayList<FileObject> list = null;
		for(String st : ks)
		{
			count++;
			if( st != null && st.contains("FullOfCrap_Index"))
			{
				out("Here: "+st+"     "+count+"    Contains string? "+input.containsKey(st));
				list = (ArrayList<FileObject>) input.get(st);
			}
			if( st != null && st.equals(path))
				out("equals");
		}
		Preferences pr = Preferences.userNodeForPackage(getClass());
		out("Prefs: "+pr.get(path, "Ouch"));
		
		//end debug
//NEED THIS		ArrayList<FileObject> list = (ArrayList<FileObject>) input.get(path);

		if( list != null)
		{
			FileObject fo = list.get(0);
			excludearray = fo.getPrefs();// grabs array of excludes from map
		}
		//index file likely moved or deleted
		else
		{
			JOptionPane.showMessageDialog(null, "The index file appears to have been moved or it has been deleted.\n" +
												"You will need to make another Exclude List.");
			excludearray = makeExcludeArray(path);
//			excludearray = null;
//			JOptionPane.showMessageDialog(null, "Error getting Exclude Array");
//			return null;
		}
	}
	return excludearray;
}

//============================================================================================================================

private String getIndexFilename(String pathtoindex) {
	String newfile = null;
	// this is the root of the drive on a Mac
	if( pathtoindex.lastIndexOf("/") == 0)							//%%%%%%%%%%%%%%% can we put this in a method and return string
	{
		newfile = user_directory + pathtoindex+"_Index.data";
//			out(user_directory + pathtoindex+"_Index.data");
	}
	else
	{
		//just adds the final part of the path of the indexed root to the filename
		//where the map is stored. Index file uses drive name to add uniqueness to name
		int slash = pathtoindex.lastIndexOf("/");
		String temp = pathtoindex.substring(slash+1, pathtoindex.length());
		newfile = user_directory + File.separator + temp + "_Index.data";
//		out(user_directory + File.separator + temp + "_Index.data");
	}
	out("Return: "+newfile);
	return newfile;
}

//============================================================================================================================

/**
 * @param path allows for a unique prefs file for this specific index of a drive
 * @return string array is the listing of excluded strings.
 * 
 * The array is a list of strings that the program tests each path for, and if .contains()
 * is true, it is skipped from being added to the index file.
 * 
 * Example list:
 * 	Containers
 * 	DocSets
 * 	Mail
 * 	frameworks
 * 	Frameworks
 * 	bundle
 * 	Contents
 *	oracle
 */
private String[] makeExcludeArray(String path)
{
	Preferences prefs = Preferences.userNodeForPackage(getClass());
	ArrayList<String> excludearraylist = new ArrayList<String>();
	String[] array = null;
	String string = "start";
	while( string != null)
	{
		if( string.equals("start"))
			string = JOptionPane.showInputDialog("Enter a substring from a path you wish to exclude from the index.\n" +
					"If you wish to exclude com.apple.xxxxxx packages, type: com.apple");
		else
			string = JOptionPane.showInputDialog("Enter another substring or press cancel to move to next step.");

		if( string != null && string.equals("start") == false && string.equals("") == false)
		{
			if( excludearraylist.contains(string))
			{
				JOptionPane.showMessageDialog(null, "That string is already in list.\nI'm skipping that duplicate");
				continue;
			}
			excludearraylist.add(string);
		}
		else if( string != null && string.equals("") == true)
			string = null;//break while loop

	}
	//stores path to index file so exclude array can be retrieved later.
	// array stored in map, not in prefs. See secondary constructor for FileObject
	prefs.put(path, path);
	out("Path stored in prefs: "+path);
	array = excludearraylist.toArray(new String[excludearraylist.size()]);


	// adds array of excluded strings to map for future retrieval
	List<FileObject> checker = map.get(path);
	FileObject fo = new FileObject(array);

	if( checker == null)
	{
		//first time this key has been processed, so add a "bucket"
		map.put(path, checker = new ArrayList<FileObject>() );
	}
	//debug
	boolean success = false;
	success = checker.add(fo);
	out("Added? "+success+"");

	return array;
}

//============================================================================================================================


/**
 * @return the result from a search operation
 */
public ArrayList<FileObject> getList()
{
	return array;
}

//============================================================================================================================

/**
 * @return used by GUI for after indexing user output
 */
public long getSize()
{
	return filesize;
}

//============================================================================================================================

/**
 * @param which is used to update cancel_pushed
 */
public static void setCancelPushed(boolean which)
{
	cancel_pushed = which;
}

//============================================================================================================================

public static boolean isRefresh() {
	return refresh_excludes;
}

public static void setRefresh(boolean which) {
	refresh_excludes = which;
}

//============================================================================================================================

/**
 * @param string just a quicker way to type System.out.println
 */
private static void out(String string)
{
	System.out.println(string);
}





}//end of code
